跳到主要内容

MySQL 学习(2) InnoDB 体系结构

InnoDB 的结构

InnoDB 由多个内存块,这些内存块共同组成了一个大的内存池

后台线程的主要作用是负责刷新内存池中的数据,保证缓冲池中的内存缓存的是最近的数据。此外,将已修改的数据文件刷新到磁盘文件,同时保证在数据库发生异常情况下 InnoDB 能恢复到正常运行状态。

后台线程

检查 InnoDB 的版本

show variables like 'innodb_version';

默认情况下,InnoDB 存储引擎的后台线程有 7 个,分别为:4 个 IO thread,1 个 Master thread,1 个锁(lock)监控线程,1 个错误监控线程。

Master 线程

这个是核心的后台线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性(包括脏页的刷新、合并插入缓冲、UNDO 页的回收)

具体的看 Master 线程那一篇博客

IO 线程

在 InnoDB 存储引擎中大量使用了 AIO (Async IO)来处理写 IO 请求,这样可以极大提高数据库的性能。而 IOThread 的工作主要是负责这些 IO 请求的回调(callback)处理。

InnoDB 1.0 版本之前共有 4 个 IO Thread,分别是 write、 read、 insert buffer 和 log IO Thread

IO thread 的数量由配置文件中的 innodb_file_io_threads 参数控制,默认为 4

不过从 InnoDB 1.x 版本开始增加了默认 IO thread 的数量,默认的 read thread 和 write thread 分别增大到了4个,并且不再使用 innodb_file_io_threads 参数, 而是分别使用 innodb_read_io_threadsinnodb_write_io_threads 参数,如下所示:

-- 使用这个命令可以查看当前 Engine
show engine innodb status;

show variables like 'innodb_version';
-- 或者
select VERSION();

可以看到读取线程 ID 总是小于写线程的 ID

log thread read thread write thread insert buffer thread

Purge 线程(打扫线程)

事务被提交后,其所使用的 undolog 可能不再需要,因此需要 PurgeThread 来回收已经使用并分配的undo页。在 InnoDB 1.1 版本之前,purge 操作仅在 InnoDB 存储引擎的 Master Thread 中完成。而从 InnoDB 1.1 版本开始,purge 操作可以独立到单独的线程中进行,以此来减轻 Master Thread 的工作,从而提高 CPU 的使用率以及提升存储引擎的性能。

用户可以在 MySQL 数据库的配置文件中添加如下命令来启用独立的 Purge

[mysqld]
innodb_purge_threads=1

不过这个参数得在 InnoDB 1.2 版本之后才支持多个 Purge 线程,设置它可以加快 undo 页的回收

内存部分

InnoDB 存储引擎的内存由以下几个部分组成:

  • 缓冲池(buffer pool);
  • 重做日志缓冲池(redo log buffer)
  • 额外的内存池(additional memory pool)

下图显示了 InnoDB 存储引擎中内存的结构情况。

它们分别由配置文件中的参数 innodb_buffer_pool_sizeinnodb_log_buffer_size 的大小决定。

show variables like 'innodb_buffer_pool_size';
show variables like 'innodb_log_buffer_size';

缓冲池是什么?

缓冲池简单来说就是一块内存区域,通过内存的速度来弥补磁盘速度较慢对数据库性能的影响。

在数据库中进行读取页的操作,首先将从磁盘读到的页存放在缓冲池中,这个过程称为将页 “FIX" 在缓冲池中。下一次再读相同的页时,首先判断该页是否在缓冲池中。若在缓冲池中,称该页在缓冲池中被命中,直接读取该页。否则,读取磁盘上的页。

对于数据库中页的修改操作,则首先修改在缓冲池中的页,然后再以一定的频率刷新到磁盘上。这里需要注意的是,页从缓冲池刷新回磁盘的操作并不是在每次页发生更新时触发,而是通过一种称为 Checkpoint 的机制刷新回磁盘。同样,这也是为了提高数据库的整体性能。

由上可知,缓冲池的大小也直接影响着数据库的整体性能,对于 InnoDB 而言,其缓冲池的配置通过 innodb_buffer_pool_size 来设置。

show variables like 'innodb_buffer_pool_size';

具体来看,缓冲池中缓存的数据页类型有:索引页、数据页、undo页、 插入缓冲(insert buffer)、自适应哈希索引(adaptive hash index)、InnoDB 存储的锁信息(lock info)、数据字典信息(data dictionary)等。

不能简单地认为,缓冲池只是缓存索引页和数据页,它们只是占缓冲池很大的一部分而已。

从 InnoDB 1.0.x 版本开始,允许有多个缓冲池实例。每个页根据哈希值平均分配到不同缓冲池实例中。这样做的好处是减少数据库内部的资源竞争,增加数据库的并发处理能力。这个数量可以通过参数 innodb_buffer_pool_instances 参数来配置(默认为 1)

show variables like 'innodb_buffer_pool_instances';

-- 使用这个命令可以查看当前 Engine 的缓冲池使用情况
show engine innodb status;

在 InnoDB 1.2 之后可以直接使用 INNODB_BUFFER_POOL_STARTS 表来查询参数:

select *
from information_schema.INNODB_BUFFER_POOL_STATS;

数据页和索引页

Page 是 Innodb 存储的最基本结构,也是 Innodb 磁盘管理的最小单位,与数据库相关的所有内容都存储在 Page 结构里。Page 分为几种类型,数据页和索引页就是其中最为重要的两种类型。

插入缓存

在 InnoDB 引擎上进行插入操作时,一般需要按照主键顺序进行插入,这样才能获得较高的插入性能。当一张表中存在非聚簇的且不唯一的索引时,在插入时,数据页的存放还是按照主键进行顺序存放,但是对于非聚簇索引叶节点的插入不再是顺序的了,这时就需要离散的访问非聚簇索引页,由于随机读取的存在导致插入操作性能下降。

InnoDB 为此设计了 Insert Buffer 来进行插入优化。

对于非聚簇索引的插入或者更新操作,不是每一次都直接插入到索引页中,而是先判断插入的非聚集索引是否在缓冲池中,

  • 若在,则直接插入;
  • 若不在,则先放入到一个 Insert Buffer 中。

看似数据库这个非聚集的索引已经查到叶节点,而实际没有,这时存放在另外一个位置。然后再以一定的频率和情况进行 Insert Buffer 和非聚簇索引页子节点的合并操作。这时通常能够将多个插入合并到一个操作中,这样就大大提高了对于非聚簇索引的插入性能。

补充:非聚集索引(非聚簇索引):以主键以外的列值作为键值构建的 B+树索引,称之为非聚集索引。非聚集索引与聚集索引的区别在于 非聚集索引的叶子节点不存储表中的数据,而是存储该列对应的主键,想要查找数据我们还需要根据主键再去聚集索引中进行查找,这个再根据聚集索引查找数据的过程,我们称为回表。

自适应哈希索引

InnoDB 会根据访问的频率和模式,为热点页建立哈希索引,来提高查询效率。InnoDB 存储引擎会监控对表上各个索引页的查询,如果观察到建立哈希索引可以带来速度上的提升,则建立哈希索引,所以叫做自适应哈希索引。

自适应哈希索引是通过缓冲池的 B+ 树页构建而来,因此建立速度很快,而且不需要对整张数据表建立哈希索引。其有一个要求,即对这个页的连续访问模式必须是一样的,也就是说其查询的条件 WHERE 必须完全一样,而且必须是连续的。

锁信息

InnoDB 存储引擎会在行级别上对表数据进行上锁。不过 InnoDB 也会在数据库内部其他很多地方使用锁,从而允许对多种不同资源提供并发访问。数据库系统使用锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性。

之后专门讲,这里先了解~

数据字典信息

InnoDB 有自己的表缓存,可以称为表定义缓存或者数据字典。当 InnoDB 打开一张表,就增加一个对应的对象到数据字典。

数据字典是对数据库中的数据、库对象、表对象等的元信息的集合。在 MySQL 中,数据字典信息内容就包括表结构、数据库名或表名、字段的数据类型、视图、索引、表字段信息、存储过程、触发器等内容。

MySQL INFORMATION_SCHEMA 库提供了对数据局元数据、统计信息、以及有关 MySQL server 的访问信息(例如:数据库名或表名,字段的数据类型和访问权限等)。该库中保存的信息也可以称为 MySQL 的数据字典。

InnoDB 是如何管理内存的?

缓冲池是一个很大的内存区域,其中存放各种类型的页。那么 InnoDB 存储引擎是怎么对这么大的内存区域进行管理的呢?

通常来说,数据库中的缓冲池是通过 LRU (Latest Recent Used,最近最少使用)算法来进行管理的。即最频繁使用的页在 LRU 列表的前端,而最少使用的页在 LRU 列表的尾端。当缓冲池不能存放新读取到的页时,将首先释放 LRU 列表中尾端的页。 在 InnoDB 存储引擎中,缓冲池中页的大小默认为 16KB,同样使用 LRU 算法对缓冲池进行管理。

稍有不同的是 InnoDB 存储引擎对传统的 LRU 算法做了一些优化。在 InnoDB 的存储引擎中,LRU 列表中还加人了 midpoint 位置(列表中间)。新读取到的页,虽然是最新访问的页,但并不是直接放人到 LRU 列表的首部,而是放人到 LRU 列表的 midpoint 位置。

这个算法在 InnoDB 存储引擎下称为 midpoint insertion strategy(中间插入策略)。在默认配置下,该位置在 LRU 列表长度的 5/8 处。midpoint 位置可由参数 innodb_old_blocks_pct 控制

show variables like 'innodb_old_blocks_pct';

这个值默认值为 37,表示新读取的页插人到 LRU 列表尾端的 37% 的位置(差不多 3/8 的位置)。在 InnoDB 存储引擎中,把 midpoint 之后的列表称为 old 列表,之前的列表称为 new 列表。可以简单地理解为 new 列表中的页都是最为活跃的热点数据。

那为什么不采用朴素的 LRU 算法,直接将读取的页放人到 LRU 列表的首部呢?

这是因为若直接将读取到的页放人到 LRU 的首部,那么某些 SQL 操作可能会使缓冲池中的页被刷新出,从而影响缓冲池的效率。常见的这类操作为索引或数据的扫描操作。这类操作需要访问表中的许多页,甚至是全部的页,而这些页通常来说又仅在这次查询操作中需要,并不是活跃的热点数据。

如果页被放人 LRU 列表的首部,那么非常可能将所需要的热点数据页从 LRU 列表中移除,而在下一次需要读取该页时,InnoDB 存储引擎需要再次访问磁盘。

总之就是避免真正的热点数据给新插入的数据被临时插入的数据挤下去了,通过 midpoint 来做个分割线

可以通过表 INNODB_BUFFER_PAGE_LRU 来观察每个 LRU 列表中每个页的具体信息,例如通过下面的语句可以看到缓冲池 LRU 列表中位置为 9326 的表的页类型:

SELECT TABLE_NAME, SPACE, PAGE_NUMBER, PAGE_TYPE
FROM information_schema.INNODB_BUFFER_PAGE_LRU WHERE SPACE = 9326;

保护热点数据 ⭐

在 Innodb 存储引擎中,采用 LRU 算法来来对热数据进行管理的,为了解决上面热点数据给挤下去的问题,InnoDB 存储引擎引人了另一个参数来进一步管理 LRU 列表,这个参数是 innodb_old_blocks_time,该参数用于表示数据页读取到 mid 位置之后,要等待多久才会加入到 LRU 队列的热数据端。(增大此值将使越来越多的块可能从缓冲池中更快地老化)

show variables like 'innodb_old_blocks_time';

默认是 1000 毫秒

因此当用户不希望那么快被丢到 old 列表那边,就适当的调小这个值

set global innodb_old_blocks_time=0

凭借这个参数,我们可以巧妙的 避免大表扫描等操作对热点数据的影响,如下:

set global innodb_old_blocks_time=1000

select * from table...

set global innodb_old_blocks_time=0

如上,先将该参数调成一个比较大的值,这样后续的全表扫描操作的热数据页不会直接转入 LRU 队列的前端,在执行完毕大查询之后,再将该值改为一个较小的值,从而避免大表扫描的影响。

脏页(dirty page)

在 LRU 列表中的页被修改后,称该页为脏页(dirty page),即缓冲池中的页和磁盘上的页的数据产生了不一致。这时数据库会通过 CheckPoint 机制将脏页刷新回磁盘,而 Flush 列表中的页即为脏页列表。

需要注意的是,脏页既存在于 LRU 列表中,也存在于 Flush 列表中。LRU 列表用来管理缓冲池中页的可用性,Flush 列表用来管理将页刷新回磁盘,二者互不影响。

1、free page:从未用过的页 2、clean page:干净的页,数据页的数据和磁盘一致 3、dirty page:脏页

之后会详细的讲脏页这块

重做日志缓冲

InnoDB 存储引擎的内存区域除了有缓冲池外,还有重做日志缓冲(redo log buffer)。InnoDB 存储引擎 首先将重做日志信息先放人到这个缓冲区,然后按一定频率将其刷新到重做日志文件。

重做日志缓冲一般不需要设置得很大,因为一般情况下每一秒钟会将重做日志缓冲刷新到日志文件,因此用户只需要保证每秒产生的事务量在这个缓冲大小之内即可。该值可由配置参数 innodb_log_buffer_size 控制,默认为 8MB:

show variables like 'innodb_log_buffer_size';

在通常情况下,8MB 的重做日志缓冲池足以满足绝大部分的应用,因为重做日志在下列三种情况下会将重做日志缓冲中的内容刷新到外部磁盘的重做日志文件中。

  • Master Thread 每一秒将重做日志缓冲刷新到重做日志文件;
  • 每个事务提交时会将重做日志缓冲刷新到重做日志文件;
  • 当重做日志缓冲池剩余空间小于 1/2 时,重做日志缓冲刷新到重做日志文件。

额外的内存池

在 InnoDB 存储引擎中,对内存的管理是通过一种称为内存堆(heap)的方式进行的。在对一些数据结构本身的内存进行分配时,需要从额外的内存池中进行申请,当该区域的内存不够时,会从缓冲池中进行申请。

例如,分配了缓冲池 innodb_buffer_pool,但是每个缓冲池中的帧缓冲(framebuffer)还有对应的缓冲控制对象(buffer control block),这些对象记录了一些诸如 LRU、锁、等待等信息,而这个对象的内存需要从额外内存池中申请。

因此,在申请了很大的 InnoDB 缓冲池时,也应考虑相应地增加这个值

Reference